/*
 * eID Test PKI Project.
 * Copyright (C) 2012 FedICT.
 * Copyright (C) 2012 Frank Cornelis.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version
 * 3.0 as published by the Free Software Foundation.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, see 
 * http://www.gnu.org/licenses/.
 */

package be.fedict.eid.pki.model;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.CRLException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.security.spec.RSAKeyGenParameterSpec;
import java.util.Enumeration;

import javax.ejb.Stateless;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.misc.MiscObjectIdentifiers;
import org.bouncycastle.asn1.misc.NetscapeCertType;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.CRLDistPoint;
import org.bouncycastle.asn1.x509.DistributionPoint;
import org.bouncycastle.asn1.x509.DistributionPointName;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.PolicyInformation;
import org.bouncycastle.asn1.x509.PolicyQualifierInfo;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.asn1.x509.X509Extension;
import org.bouncycastle.cert.CertIOException;
import org.bouncycastle.cert.X509CRLHolder;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v2CRLBuilder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.util.PrivateKeyFactory;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder;
import org.joda.time.DateTime;

import be.fedict.eid.pki.exception.InvalidPolicyException;
import be.fedict.eid.pki.exception.InvalidX500NameException;

@Stateless
public class PKIGeneratorBean {

	private static final Log LOG = LogFactory.getLog(PKIGeneratorBean.class);

	public KeyPair generateKeyPair(int keySize) {
		LOG.debug("generating key pair: " + keySize);
		KeyPairGenerator keyPairGenerator;
		try {
			keyPairGenerator = KeyPairGenerator.getInstance("RSA");
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException("RSA algo not available", e);
		}
		SecureRandom random = new SecureRandom();
		try {
			keyPairGenerator.initialize(new RSAKeyGenParameterSpec(keySize,
					RSAKeyGenParameterSpec.F4), random);
		} catch (InvalidAlgorithmParameterException e) {
			throw new RuntimeException("unsupported key size: " + keySize);
		}
		KeyPair keyPair = keyPairGenerator.generateKeyPair();
		return keyPair;
	}

	public X509Certificate generateCACertificate(KeyPair keyPair,
			String subject, DateTime notBefore, DateTime notAfter,
			String policyOid, String cps) throws InvalidX500NameException,
			InvalidPolicyException {
		LOG.debug("generate CA certificate: " + subject);

		X500Name issuer;
		try {
			issuer = new X500Name(subject);
		} catch (IllegalArgumentException e) {
			throw new InvalidX500NameException();
		}
		X500Name subjectX500Name = new X500Name(subject);

		SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo
				.getInstance(keyPair.getPublic().getEncoded());

		SecureRandom secureRandom = new SecureRandom();
		byte[] serialValue = new byte[8];
		secureRandom.nextBytes(serialValue);
		BigInteger serial = new BigInteger(serialValue);

		X509v3CertificateBuilder x509v3CertificateBuilder = new X509v3CertificateBuilder(
				issuer, serial, notBefore.toDate(), notAfter.toDate(),
				subjectX500Name, publicKeyInfo);

		try {
			JcaX509ExtensionUtils extensionUtils = new JcaX509ExtensionUtils();
			x509v3CertificateBuilder.addExtension(
					X509Extension.subjectKeyIdentifier, false, extensionUtils
							.createSubjectKeyIdentifier(keyPair.getPublic()));
			x509v3CertificateBuilder.addExtension(
					X509Extension.authorityKeyIdentifier, false, extensionUtils
							.createAuthorityKeyIdentifier(keyPair.getPublic()));

			x509v3CertificateBuilder.addExtension(
					MiscObjectIdentifiers.netscapeCertType, false,
					new NetscapeCertType(NetscapeCertType.sslCA
							| NetscapeCertType.smimeCA
							| NetscapeCertType.objectSigningCA));

			x509v3CertificateBuilder.addExtension(X509Extension.keyUsage, true,
					new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign));

			x509v3CertificateBuilder.addExtension(
					X509Extension.basicConstraints, true, new BasicConstraints(
							2147483647));

		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		PolicyQualifierInfo policyQualifierInfo = new PolicyQualifierInfo(cps);
		ASN1ObjectIdentifier policyObjectIdentifier;
		try {
			policyObjectIdentifier = new ASN1ObjectIdentifier(policyOid);
		} catch (IllegalArgumentException e) {
			throw new InvalidPolicyException();
		}
		PolicyInformation policyInformation = new PolicyInformation(
				policyObjectIdentifier, new DERSequence(policyQualifierInfo));
		try {
			x509v3CertificateBuilder.addExtension(
					X509Extension.certificatePolicies, false, new DERSequence(
							policyInformation));
		} catch (CertIOException e) {
			throw new RuntimeException(e);
		}

		AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder()
				.find("SHA1withRSA");
		AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder()
				.find(sigAlgId);
		AsymmetricKeyParameter asymmetricKeyParameter;
		try {
			asymmetricKeyParameter = PrivateKeyFactory.createKey(keyPair
					.getPrivate().getEncoded());
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
		ContentSigner contentSigner;
		try {
			contentSigner = new BcRSAContentSignerBuilder(sigAlgId, digAlgId)
					.build(asymmetricKeyParameter);
		} catch (OperatorCreationException e) {
			throw new RuntimeException(e);
		}
		X509CertificateHolder x509CertificateHolder = x509v3CertificateBuilder
				.build(contentSigner);

		byte[] encodedCertificate;
		try {
			encodedCertificate = x509CertificateHolder.getEncoded();
		} catch (IOException e) {
			throw new RuntimeException(e);
		}

		CertificateFactory certificateFactory;
		try {
			certificateFactory = CertificateFactory.getInstance("X.509");
		} catch (CertificateException e) {
			throw new RuntimeException(e);
		}
		X509Certificate certificate;
		try {
			certificate = (X509Certificate) certificateFactory
					.generateCertificate(new ByteArrayInputStream(
							encodedCertificate));
		} catch (CertificateException e) {
			throw new RuntimeException(e);
		}
		return certificate;
	}

	public X509CRL generateCrl(X509Certificate issuerCertificate,
			PrivateKey issuerPrivateKey, DateTime thisUpdate,
			DateTime nextUpdate) {
		JcaX509CertificateHolder x509CertificateHolder;
		try {
			x509CertificateHolder = new JcaX509CertificateHolder(
					issuerCertificate);
		} catch (CertificateEncodingException e) {
			throw new RuntimeException(e);
		}
		X500Name issuerName = x509CertificateHolder.getIssuer();
		X509v2CRLBuilder x509v2crlBuilder = new X509v2CRLBuilder(issuerName,
				thisUpdate.toDate());
		x509v2crlBuilder.setNextUpdate(nextUpdate.toDate());
		AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder()
				.find("SHA1withRSA");
		AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder()
				.find(sigAlgId);
		AsymmetricKeyParameter asymmetricKeyParameter;
		try {
			asymmetricKeyParameter = PrivateKeyFactory
					.createKey(issuerPrivateKey.getEncoded());
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
		ContentSigner contentSigner;
		try {
			contentSigner = new BcRSAContentSignerBuilder(sigAlgId, digAlgId)
					.build(asymmetricKeyParameter);
		} catch (OperatorCreationException e) {
			throw new RuntimeException(e);
		}
		X509CRLHolder x509crlHolder = x509v2crlBuilder.build(contentSigner);
		byte[] crlValue;
		try {
			crlValue = x509crlHolder.getEncoded();
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
		CertificateFactory certificateFactory;
		try {
			certificateFactory = CertificateFactory.getInstance("X.509");
		} catch (CertificateException e) {
			throw new RuntimeException(e);
		}
		X509CRL crl;
		try {
			crl = (X509CRL) certificateFactory
					.generateCRL(new ByteArrayInputStream(crlValue));
		} catch (CRLException e) {
			throw new RuntimeException(e);
		}
		return crl;
	}

	public X509Certificate generateIntermediateCACertificate(
			X509Certificate issuerCertificate, DateTime notBefore,
			DateTime notAfter, String subject, PublicKey subjectPublicKey,
			String policyOid, PrivateKey issuerPrivateKey, String crlUrl)
			throws InvalidX500NameException, InvalidPolicyException {
		JcaX509CertificateHolder jcaX509CertificateHolder;
		try {
			jcaX509CertificateHolder = new JcaX509CertificateHolder(
					issuerCertificate);
		} catch (CertificateEncodingException e) {
			throw new RuntimeException(e);
		}
		X500Name issuerName = jcaX509CertificateHolder.getIssuer();

		SecureRandom secureRandom = new SecureRandom();
		byte[] serialValue = new byte[16];
		secureRandom.nextBytes(serialValue);
		BigInteger serial = new BigInteger(serialValue);

		X500Name subjectX500Name;
		try {
			subjectX500Name = new X500Name(subject);
		} catch (IllegalArgumentException e) {
			throw new InvalidX500NameException();
		}

		SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo
				.getInstance(subjectPublicKey.getEncoded());

		X509v3CertificateBuilder x509v3CertificateBuilder = new X509v3CertificateBuilder(
				issuerName, serial, notBefore.toDate(), notAfter.toDate(),
				subjectX500Name, publicKeyInfo);

		try {
			JcaX509ExtensionUtils extensionUtils = new JcaX509ExtensionUtils();
			x509v3CertificateBuilder
					.addExtension(
							X509Extension.subjectKeyIdentifier,
							false,
							extensionUtils
									.createSubjectKeyIdentifier(subjectPublicKey));
			PublicKey issuerPublicKey = issuerCertificate.getPublicKey();
			x509v3CertificateBuilder.addExtension(
					X509Extension.authorityKeyIdentifier, false, extensionUtils
							.createAuthorityKeyIdentifier(issuerPublicKey));

			x509v3CertificateBuilder.addExtension(
					MiscObjectIdentifiers.netscapeCertType, false,
					new NetscapeCertType(NetscapeCertType.sslCA
							| NetscapeCertType.smimeCA
							| NetscapeCertType.objectSigningCA));

			x509v3CertificateBuilder.addExtension(X509Extension.keyUsage, true,
					new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign));

			x509v3CertificateBuilder.addExtension(
					X509Extension.basicConstraints, true, new BasicConstraints(
							0));

			GeneralName generalName = new GeneralName(
					GeneralName.uniformResourceIdentifier, new DERIA5String(
							crlUrl));
			GeneralNames generalNames = new GeneralNames(generalName);
			DistributionPointName distPointName = new DistributionPointName(
					generalNames);
			DistributionPoint distPoint = new DistributionPoint(distPointName,
					null, null);
			DistributionPoint[] crlDistPoints = new DistributionPoint[] { distPoint };
			CRLDistPoint crlDistPoint = new CRLDistPoint(crlDistPoints);
			x509v3CertificateBuilder.addExtension(
					X509Extension.cRLDistributionPoints, false, crlDistPoint);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}

		byte[] issuerCertificatePoliciesExtensionValue = issuerCertificate
				.getExtensionValue(X509Extension.certificatePolicies.getId());
		PolicyInformation issuerPolicyInformation;
		try {
			issuerPolicyInformation = PolicyInformation
					.getInstance(((ASN1Sequence) new ASN1InputStream(
							((DEROctetString) new ASN1InputStream(
									issuerCertificatePoliciesExtensionValue)
									.readObject()).getOctets()).readObject())
							.getObjectAt(0));
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
		ASN1Sequence policyQualifiers = issuerPolicyInformation
				.getPolicyQualifiers();
		Enumeration policyQualifiersEnum = policyQualifiers.getObjects();
		String cps = null;
		while (policyQualifiersEnum.hasMoreElements()) {
			PolicyQualifierInfo policyQualifierInfo = PolicyQualifierInfo
					.getInstance(policyQualifiersEnum.nextElement());
			DERIA5String cpsString = DERIA5String
					.getInstance(policyQualifierInfo.getQualifier());
			cps = cpsString.getString();
			LOG.debug("issuer CPS: " + cps);
		}

		PolicyQualifierInfo policyQualifierInfo = new PolicyQualifierInfo(cps);
		ASN1ObjectIdentifier policyObjectIdentifier;
		try {
			policyObjectIdentifier = new ASN1ObjectIdentifier(policyOid);
		} catch (IllegalArgumentException e) {
			throw new InvalidPolicyException();
		}
		PolicyInformation policyInformation = new PolicyInformation(
				policyObjectIdentifier, new DERSequence(policyQualifierInfo));

		try {
			x509v3CertificateBuilder.addExtension(
					X509Extension.certificatePolicies, false, new DERSequence(
							policyInformation));
		} catch (CertIOException e) {
			throw new RuntimeException(e);
		}

		AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder()
				.find("SHA1withRSA");
		AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder()
				.find(sigAlgId);
		AsymmetricKeyParameter asymmetricKeyParameter;
		try {
			asymmetricKeyParameter = PrivateKeyFactory
					.createKey(issuerPrivateKey.getEncoded());
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
		ContentSigner contentSigner;
		try {
			contentSigner = new BcRSAContentSignerBuilder(sigAlgId, digAlgId)
					.build(asymmetricKeyParameter);
		} catch (OperatorCreationException e) {
			throw new RuntimeException(e);
		}
		X509CertificateHolder x509CertificateHolder = x509v3CertificateBuilder
				.build(contentSigner);

		byte[] encodedCertificate;
		try {
			encodedCertificate = x509CertificateHolder.getEncoded();
		} catch (IOException e) {
			throw new RuntimeException(e);
		}

		CertificateFactory certificateFactory;
		try {
			certificateFactory = CertificateFactory.getInstance("X.509");
		} catch (CertificateException e) {
			throw new RuntimeException(e);
		}
		X509Certificate certificate;
		try {
			certificate = (X509Certificate) certificateFactory
					.generateCertificate(new ByteArrayInputStream(
							encodedCertificate));
		} catch (CertificateException e) {
			throw new RuntimeException(e);
		}
		return certificate;
	}
}
